Nathan Trouvain
Inria - Mnemosyne
Un exemple: l'attracteur de Lorenz
$$ \begin{split} \dot{x}(t) &= \sigma (y(t) - x(t)) \\ \dot{y}(t) &= \rho x(t) - y(t) - x(t)z(t) \\ \dot{z}(t) &= x(t)y(t) - \beta z(t) \end{split} $$from reservoirpy.datasets import lorenz
timesteps = 3000
X = lorenz(timesteps, x0=[17.67, 12.93, 43.91])
plot_lorenz(X, 1000)
Connaissant la valeur de la séries à l'instant $t$:
Prédire $P(t + 10)$ connaissant $P(t)$.
from reservoirpy.datasets import to_forecasting
x, y = to_forecasting(X, forecast=10)
X_train1, y_train1 = x[:2000], y[:2000]
X_test1, y_test1 = x[2000:], y[2000:]
plot_train_test(X_train1, y_train1, X_test1, y_test1)
units = 100 # - nombre de neurones
leak_rate = 0.3 # - leaking rate
spectral_radius = 0.95 # - rayon spectral
input_scaling = 0.5 # - facteur de mise à l'échelle des entrées (input scaling)
connectivity = 0.1 # - densité des connexions du reservoir vers lui même
input_connectivity = 0.2 # et des entrées vers le reservoir
regularization = 1e-4 # - coefficient de régularisation (L2)
transient = 100 # - temps de chauffe du reservoir
seed = 1234 # - reproductibilité
from reservoirpy.nodes import Reservoir, Ridge
reservoir = Reservoir(units, input_scaling=input_scaling, sr=spectral_radius,
lr=leak_rate, rc_connectivity=connectivity,
input_connectivity=input_connectivity, seed=seed)
readout = Ridge(ridge=regularization)
esn = reservoir >> readout
reservoir_fb = reservoir << readout
esn_fb = reservoir_fb >> readout
L'apprentissage est offline ("hors-ligne") : il n'a lieu qu'une seule fois, sur l'ensemble des données d'entraînement.
esn.fit(X_train1, y_train1, warmup=transient);
plot_readout(readout)
y_pred1 = esn.run(X_test1)
plot_results(y_pred1, y_test1)
Coefficient de détermination $R^2$ et erreur quadratique normalisée :
rsquare(y_test1, y_pred1), nrmse(y_test1, y_pred1)
(0.9961759934656298, 0.012317846485056738)
units = 300 # - nombre de neurones
leak_rate = 0.3 # - leaking rate
spectral_radius = 1.25 # - rayon spectral
input_scaling = 0.1 # - facteur de mise à l'échelle des entrées (input scaling)
connectivity = 0.1 # - densité des connexions du reservoir vers lui même
input_connectivity = 0.2 # et des entrées vers le reservoir
regularization = 1e-4 # - coefficient de régularisation (L2)
transient = 100 # - temps de chauffe du reseroivr
seed = 1234 # - reproductibilité
esn = reset_esn()
x, y = to_forecasting(X, forecast=1)
X_train3, y_train3 = x[:2000], y[:2000]
X_test3, y_test3 = x[2000:], y[2000:]
esn = esn.fit(X_train3, y_train3, warmup=transient)
seed_timesteps = 100
warming_inputs = X_test3[:seed_timesteps]
warming_out = esn.run(warming_inputs) # échauffement
nb_generations = 500
X_gen = np.zeros((nb_generations, 3))
y = warming_out[-1]
for t in range(nb_generations): # génération
y = esn(y)
X_gen[t, :] = y
X_t = X_test3[seed_timesteps: nb_generations+seed_timesteps]
plot_generation(X_gen, X_t, warming_out=warming_out, warming_inputs=warming_inputs)
plot_attractors(X_gen, X_t, warming_inputs, warming_out)
Apprentissage se déroulant de manière incrémentale.
Utilisation de l'algorithme Recursive Least Squares
from reservoirpy.nodes import RLS
reservoir = Reservoir(units, input_scaling=input_scaling, sr=spectral_radius,
lr=leak_rate, rc_connectivity=connectivity,
input_connectivity=input_connectivity, seed=seed)
readout = RLS()
esn_online = reservoir >> readout
outputs_pre = np.zeros(X_train1.shape)
for t, (x, y) in enumerate(zip(X_train1, y_train1)): # pour chaque pas de temps de la série :
prediction = esn_online.train(np.atleast_2d(x), np.atleast_2d(y))
outputs_pre[t, :] = prediction
plot_results(outputs_pre, y_train1, sample=100)
plot_results(outputs_pre, y_train1, sample=500)
esn_online.train(X_train1, y_train1)
pred_online = esn_online.run(X_test1) # Wout est maintenant figée
plot_results(pred_online, y_test1, sample=500)
Determination coefficient $R^2$ and NRMSE:
rsquare(y_test1, pred_online), nrmse(y_test1, pred_online)
(0.9949772578256109, 0.014117121497420036)
units = 300 # - nombre de neurones
leak_rate = 0.3 # - leaking rate
spectral_radius = 1.25 # - rayon spectral
input_scaling = 0.1 # - facteur de mise à l'échelle des entrées (input scaling)
connectivity = 0.1 # - densité des connexions du reservoir vers lui même
input_connectivity = 0.2 # et des entrées vers le reservoir
regularization = 1e-4 # - coefficient de régularisation (L2)
transient = 100 # - temps de chauffe du reseroivr
seed = 1234 # - reproductibilité
Le rayon spectral est la valeur propre maximale de la matrice des poids du réservoir ($W$).
states = []
radii = [0.1, 1.25, 10.0]
for sr in radii:
reservoir = Reservoir(units, sr=sr, input_scaling=0.001, lr=leak_rate, rc_connectivity=connectivity,
input_connectivity=input_connectivity)
s = reservoir.run(X_test1[:500])
states.append(s)
units_nb = 20
plt.figure(figsize=(15, 8))
for i, s in enumerate(states):
plt.subplot(len(radii)*100+10+i+1)
plt.plot(s[:, :units_nb], alpha=0.6)
plt.ylabel(f"$sr={radii[i]}$")
plt.xlabel(f"Activations ({units_nb} neurons)")
plt.show()
$-$ rayon spectral $\rightarrow$ dynamiques stables
$+$ rayon spectral $\rightarrow$ dynamiques chaotiques
Il s'agit d'un coefficient appliqué à $W_{in}$, venant changer l'échelle des données en entrée.
states = []
scalings = [0.00001, 0.001, 1.0]
for iss in scalings:
reservoir = Reservoir(units, sr=spectral_radius, input_scaling=iss, lr=leak_rate,
rc_connectivity=connectivity, input_connectivity=input_connectivity)
s = reservoir.run(X_test1[:500])
states.append(s)
units_nb = 20
plt.figure(figsize=(15, 8))
for i, s in enumerate(states):
plt.subplot(len(scalings)*100+10+i+1)
plt.plot(s[:, :units_nb], alpha=0.6)
plt.ylabel(f"$iss={scalings[i]}$")
plt.xlabel(f"Activations ({units_nb} neurons)")
plt.show()
Correlation moyenne des activités des neurones du reservoir avec les entrées :
for i, s in enumerate(states):
corr = correlation(states[i], X_test1[:500])
print(f"IS : {scalings[i]}, correlation max : {corr}")
IS : 1e-05, correlation max : 5654.146949848663 IS : 0.001, correlation max : 5658.500729694261 IS : 1.0, correlation max : 5804.2333455595935
L'input scaling peut aussi être utilisé pour ajuster l'influence de chaque donnée en entrée.
avec $\alpha \in [0, 1]$ et:
$$ f(u, x) = \tanh(W_{in} \cdotp u + W \cdotp x) $$states = []
rates = [0.02, 0.2, 0.9]
for lr in rates:
reservoir = Reservoir(units, sr=spectral_radius, input_scaling=input_scaling, lr=lr,
rc_connectivity=connectivity, input_connectivity=input_connectivity)
s = reservoir.run(X_test1[:500])
states.append(s)
units_nb = 20
plt.figure(figsize=(15, 8))
for i, s in enumerate(states):
plt.subplot(len(rates)*100+10+i+1)
plt.plot(s[:, :units_nb] + 2*i)
plt.ylabel(f"$lr={rates[i]}$")
plt.xlabel(f"States ({units_nb} neurons)")
plt.show()
Le leaking rate contrôle la "mémoire" de l'ESN. Il peut être vu comme l'inverse de sa constante de temps
features = ['com_x', 'com_y', 'com_z', 'trunk_pitch', 'trunk_roll', 'left_x', 'left_y',
'right_x', 'right_y', 'left_ankle_pitch', 'left_ankle_roll', 'left_hip_pitch',
'left_hip_roll', 'left_hip_yaw', 'left_knee', 'right_ankle_pitch',
'right_ankle_roll', 'right_hip_pitch', 'right_hip_roll',
'right_hip_yaw', 'right_knee']
prediction = ['fallen']
force = ['force_orientation', 'force_magnitude']
plot_robot(Y, Y_train, F)
Utilisation de l'objet ESN, modèle optimisé et distribué pour les Echo State Networks: permet la parallelisation des calculs
from reservoirpy.nodes import ESN
reservoir = Reservoir(300, lr=0.5, sr=0.99, input_bias=False)
readout = Ridge(ridge=1e-3)
esn = ESN(reservoir=reservoir, readout=readout, workers=-1) # version distribuée
esn = esn.fit(X_train, y_train)
res = esn.run(X_test)
plot_robot_results(y_test, res)
print("RMSE moyenne :", f"{np.mean(scores):.4f}", "±", f"{np.std(scores):.5f}")
print("RMSE moyenne (avec seuil) :", f"{np.mean(filt_scores):.4f}", "±", f"{np.std(filt_scores):.5f}")
RMSE moyenne : 0.1693 ± 0.10344 RMSE moyenne (avec seuil) : 0.1443 ± 0.15187
Les données peuvent être téléchargées sur Zenodo : https://zenodo.org/record/4736597
Plusieurs motifs temporels répétitifs differents à décoder : les phrases.
im = plt.imread("./static/canary_outputs.png")
plt.figure(figsize=(15, 15)); plt.imshow(im); plt.axis('off'); plt.show()
esn = esn.fit(X_train, y_train)
outputs = esn.run(X_test)
scores # pour chaque chant testé
[0.9494949494949495, 0.9085303186022611, 0.9531759739013625, 0.9410002304678498, 0.9638157894736842, 0.9478260869565217, 0.892835458409229, 0.9403111739745403, 0.9212787899621864, 0.8765393116514051]
print("Précision moyenne :", f"{np.mean(scores):.4f}", "±", f"{np.std(scores):.5f}")
Précision moyenne : 0.9295 ± 0.02716
Analyse des données de Maas et al. (2011): IMDB dataset.
Composé d'avis attribués à des films par les utilisateurs de IMDB.
Objectif: attribuer un score de 0 ou 1, aux commentaires respectivements positifs et négatifs.
import spacy
nlp = spacy.load("en_core_web_lg")
sad_doc = nlp("I hate everything about this movie.")
happy_doc = nlp("I really liked this movie!")
unrelated_doc = nlp("Dogs usually hate cats.")
print("Similarités: ", sad_doc.similarity(happy_doc), sad_doc.similarity(unrelated_doc))
Similarités: 0.9271912733659942 0.5860950057882411
for token in sad_doc:
print(token)
I hate everything about this movie .
print(token.vector)
[-0.076454 -4.6896 -4.0431 -3.4333 11.758 3.7212 -0.98133 2.7902 0.43608 -2.4425 6.9311 2.7647 -1.7035 6.3123 2.6301 1.1136 -3.3689 -5.4062 -3.6021 -9.8479 4.6016 -0.50542 0.66145 -3.552 -4.3909 -2.3702 -1.8251 -4.8768 2.7245 5.3177 -0.50146 -7.2556 -4.6895 -5.4604 -5.6257 1.6121 -0.8789 1.6936 1.9308 6.7769 3.6739 0.28693 -0.22849 -4.0779 -4.7086 2.0722 -2.3405 -1.6884 -1.9876 -2.2207 -2.6514 3.3686 0.12579 -5.8689 4.6715 1.1999 -4.3008 2.1239 0.029298 3.0072 -2.4435 -3.9103 1.0187 -2.0263 5.0496 0.66619 -3.978 -4.0066 -1.8538 0.65539 -2.1525 -9.2576 -2.6783 -1.4023 4.0334 -2.3129 -5.1051 -0.91791 0.11236 2.626 -5.5713 -2.7705 -2.5144 0.069018 6.1091 -3.0522 0.025957 -3.9766 -0.42248 0.60821 -4.883 2.8595 0.046312 -5.0708 -0.87191 -3.0453 -1.5862 -2.7432 3.6893 0.69812 6.3418 2.264 2.7177 4.6719 2.9297 1.1045 2.3091 -0.52648 -2.776 -2.0394 -4.1364 0.091809 -0.62613 -0.14244 1.4294 5.0263 4.395 -0.63146 3.0023 -3.5179 -0.71451 -7.009 -0.08824 3.5531 3.1061 -6.3624 6.6491 -4.8624 0.20515 0.17462 -3.203 1.4741 4.2968 -4.4224 0.31291 -0.34638 -6.8143 -6.1415 3.9493 -5.1503 0.29465 -2.0811 -2.1067 2.1568 -3.5174 1.6831 -2.5722 -4.3133 3.0666 -1.7038 -2.353 0.81963 2.6208 5.6669 -1.8992 0.57382 5.4505 3.6633 -4.7065 0.60155 -0.53312 -3.1413 -2.4749 8.7393 -1.3324 -4.1015 -2.9628 -0.079662 -1.5063 3.3633 7.0645 1.2196 4.3788 -6.4489 1.2194 0.37559 -1.2663 2.947 -2.7332 -3.5665 -1.3088 0.19732 -0.77669 -6.1613 -4.2444 -0.68401 -0.61397 0.030807 -0.12418 2.8148 -2.2854 -2.83 -1.9922 -7.3996 -2.8544 -3.7009 -0.47745 1.5749 -1.9741 -1.5416 0.45149 -0.21663 1.1495 -5.0475 -0.19798 0.19929 -3.7595 2.5563 -3.8548 -2.7141 2.9688 -1.3176 -1.2686 -1.3209 -2.7947 3.6765 -6.7201 -3.5003 -0.1796 1.1076 -1.0369 -0.27029 1.3922 0.1615 -1.952 -3.9036 1.1277 2.3389 5.1609 2.1905 2.9146 -8.1347 0.68726 2.8535 1.4085 5.4403 -9.4458 2.8775 -0.66906 -1.1599 -3.3115 -3.5608 1.5127 -3.1845 -9.1884 0.6305 -5.6819 -0.12861 -2.3537 1.0302 -0.76796 -0.83891 -4.1329 1.773 -0.073686 -1.2361 5.0983 -3.4001 0.42573 -1.9998 1.439 4.7645 6.0452 4.9381 2.3885 -1.0576 -2.0167 -0.4184 -1.8464 2.4374 2.3647 1.2872 -1.7353 1.6065 -0.98225 2.762 1.4601 -0.5405 0.26084 8.2027 0.97069 3.2362 -5.9116 3.7992 -0.31084 -1.3803 -3.0381 0.19792 -1.6612 0.39299 -3.7749 3.0147 1.8615 -3.9556 1.134 3.0243 -3.0984 1.304 -0.52699 -1.3622 ]
Taille du jeu de données:
Chaque avis a une longueur N, son nombre de tokens. Chaque token est encodé par un vecteur de 300 dimensions.
from reservoirpy.nodes import Reservoir, Ridge
reservoir = Reservoir(500, sr=0.9, input_scaling=0.01, lr=0.1, input_bias=False)
readout = Ridge(ridge=1e-8)
esn = reservoir >> readout
esn.fit(X_train, y_train)
'Model-5': Model('Reservoir-16', 'Ridge-4')
On obtient un score par pas de temps. Pour réduire: on prend le maximum de score sur tous les pas de temps, et on applique un seuil.
y_pred = esn.run(X_test)
threshold = 0.85 # choisi grâce à la ROC
plot_esn_response()
y_pred_thr = [1 if y.max() > threshold else 0 for y in y_pred]
y_test_thr = [1 if y.max() > threshold else 0 for y in y_test]
from sklearn.metrics import accuracy_score
score = accuracy_score(y_test_thr, y_pred_thr)
print("Précision: ", score)
Précision: 0.7685